/*
 * topological-hex, a program to compute combinatorial hexahedral meshes.
 *
 * Copyright (C) <2018> <Université catholique de Louvain (UCL), Belgique>
 *
 * List of the contributors to the development, description and complete
 * License: see LICENSE file.
 *
 * This program (topological-hex) is free software:
 * you can redistribute it and/or modify it under the terms
 * of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program (see COPYING file).  If not,
 * see <http://www.gnu.org/licenses/>.
 */

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <omp.h>

#include "parallel_dfs.h"
#include "lower_bound.h"

typedef struct options {
  const char *filename;
  const char *symmetries_filename;
  const char *precomputed_filename;
  const char *lower_bounds_filename;
  const char *server_name;

  vertex_index max_v_int;
  hex_index max_hex;

  size_t chunk_size;
  size_t num_subdivisions;

  uint8_t flags;
} options;

static
void parse_options(int argc, char **argv, options *opts);

static void on_print(const char *message, void *data) {
  printf("%s", message);
}

int main(int argc, char **argv) {
  options opts;
  parse_options(argc, argv, &opts);

  if (opts.lower_bounds_filename) {
    if (load_lower_bounds(opts.lower_bounds_filename) != SUCCESS)
      fprintf(stderr, "%s: failed to load lower bounds from file `%s'.\n",
              argv[0], opts.lower_bounds_filename);
  }

  if (opts.precomputed_filename) {
    if (load_known_solutions(opts.precomputed_filename) != SUCCESS)
      fprintf(stderr, "%s: failed to load precomputed solutions from file `%s'.\n",
              argv[0], opts.precomputed_filename);
  }

  if (!opts.filename) {
    if (solver_run_distributed_client(opts.server_name, on_print, NULL,
                                      opts.chunk_size) != SUCCESS) {
      fprintf(stderr, "Failed to run client connected to server %s\n", argv[1]);
      return 1;
    }

    return 0;
  }
  else {
    int status = 0;

    mesh mesh;
    if (mesh_load(&mesh, opts.filename) != SUCCESS) {
      status = 1;
      fprintf(stderr, "Failed to load mesh from file: %s\n", argv[0]);
      goto fail_mesh_load;
    }

    if (opts.max_hex < mesh.num_quads / 6) opts.max_hex = mesh.num_quads / 6;

    if (mesh_reserve(&mesh, opts.max_hex) != SUCCESS) {
      status = 1;
      fprintf(stderr, "Failed to reserve memory for hexes\n");
      goto fail_mesh_reserve;
    }

    uint32_t num_v = mesh.num_vertices + opts.max_v_int;
    if (num_v >= VERTEX_SET_MAX) num_v = VERTEX_SET_MAX - 1;


    solver solver;
    if (solver_init(&solver, &mesh, num_v, opts.flags) != SUCCESS) {
      status = 1;
      fprintf(stderr, "Failed to allocate memory for the solver\n");
      goto fail_solver_init;
    }

    if (opts.symmetries_filename) {
      vertex_index *symmetries;
      vertex_index num_boundary_vertices;
      size_t num_symmetries;

      if (read_symmetries(opts.symmetries_filename, &symmetries, &num_boundary_vertices,
                          &num_symmetries) == SUCCESS) {
        solver_set_symmetries(&solver, num_symmetries, num_boundary_vertices,
                              symmetries);
      }
      else {
        fprintf(stderr, "Failed to load symmetries from file: `%s'\n",
                opts.symmetries_filename);
        solver_compute_symmetries(&solver);
      }
    }
    else
      solver_compute_symmetries(&solver);

    if (solver_run_distributed(
          &solver, opts.server_name,
          on_print, NULL, opts.chunk_size, opts.num_subdivisions) != SUCCESS) {
      status = 1;
      fprintf(stderr, "Failed to run client connect to server %s\n",
              opts.server_name);
      goto fail_run_client;
    }

  fail_run_client:
    solver_release(&solver);
  fail_solver_init:
  fail_mesh_reserve:
    mesh_release(&mesh);
  fail_mesh_load:
    release_known_solutions();
    release_lower_bounds();
    return status;
  }
}

static
void print_usage(const char *program_name, int status);

static
void parse_options(int argc, char **argv, options *opts) {
  opts->filename = NULL;
  opts->symmetries_filename = NULL;
  opts->precomputed_filename = NULL;
  opts->lower_bounds_filename = NULL;
  opts->server_name = NULL;

  opts->max_hex = 64;
  opts->max_v_int = VERTEX_SET_MAX;

  opts->flags = SOLVER_BIPARTITE_GRAPH;

  opts->chunk_size = omp_get_max_threads() * 8;
  opts->num_subdivisions = 512 * 1024;

  for (int i = 1; i < argc; ) {
    if (strcmp(argv[i], "-H") == 0 || strcmp(argv[i], "--h-max") == 0) {
      if (i + 1 == argc) print_usage(argv[0], 1);

      char *end = NULL;
      opts->max_hex = strtoul(argv[i + 1], &end, 10);
      if (!end || *end != '\0') {
        fprintf(stderr, "%s: error: invalid limit to the number of hexahedra `%s'\n",
                argv[0], argv[i+1]);
        print_usage(argv[0], 1);
      }

      memmove(argv + i, argv + i + 2, (argc - i - 2) * sizeof(*argv));
      argc -= 2;
    }
    else if (strcmp(argv[i], "-V") == 0 || strcmp(argv[i], "--v-max") == 0) {
      if (i + 1 == argc) print_usage(argv[0], 1);

      char *end = NULL;
      opts->max_v_int = strtoul(argv[i + 1], &end, 10);
      if (!end || *end != '\0') {
        fprintf(stderr, "%s: error: invalid limit to the number of vertices `%s'\n",
                argv[0], argv[i+1]);
        print_usage(argv[0], 1);
      }

      memmove(argv + i, argv + i + 2, (argc - i - 2) * sizeof(*argv));
      argc -= 2;
    }
    else if (strcmp(argv[i], "-S") == 0 || strcmp(argv[i], "--symmetries") == 0) {
      if (i + 1 == argc) print_usage(argv[0], 1);
      opts->symmetries_filename = argv[i + 1];
      memmove(argv + i, argv + i + 2, (argc - i - 2) * sizeof(*argv));
      argc -= 2;
    }
    else if (strcmp(argv[i], "-s") == 0 || strcmp(argv[i], "--shellable-only") == 0) {
      opts->flags |= SOLVER_SHELLABLE_ONLY;
      memmove(argv + i, argv + i + 1, (argc - i - 1) * sizeof(*argv));
      argc--;
    }
    else if (strcmp(argv[i], "--non-bipartite-graph") == 0) {
      opts->flags &= ~SOLVER_BIPARTITE_GRAPH;
      memmove(argv + i, argv + i + 1, (argc - i - 1) * sizeof(*argv));
      argc--;
    }
    else if (strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--precomputed") == 0) {
      if (i + 1 == argc) print_usage(argv[0], 1);
      opts->precomputed_filename = argv[i + 1];
      memmove(argv + i, argv + i + 2, (argc - i - 2) * sizeof(*argv));
      argc -= 2;
    }
    else if (strcmp(argv[i], "-L") == 0 || strcmp(argv[i], "--lower-bounds") == 0) {
      if (i + 1 == argc) print_usage(argv[0], 1);
      opts->lower_bounds_filename = argv[i + 1];
      memmove(argv + i, argv + i + 2, (argc - i - 2) * sizeof(*argv));
      argc -= 2;
    }
    else if (strcmp(argv[i], "--server") == 0) {
      if (i + 1 == argc) print_usage(argv[0], 1);
      opts->server_name = argv[i + 1];
      memmove(argv + i, argv + i + 2, (argc - i - 2) * sizeof(*argv));
      argc -= 2;
    }
    else if (strcmp(argv[i], "-C") == 0 || strcmp(argv[i], "--chunk-size") == 0) {
      if (i + 1 == argc) print_usage(argv[0], 1);

      char *end = NULL;
      opts->chunk_size = strtoul(argv[i + 1], &end, 10);
      if (!end || *end != '\0' || opts->chunk_size == 0) {
        fprintf(stderr, "%s: error: invalid chunk size `%s'\n",
                argv[0], argv[i+1]);
        print_usage(argv[0], 1);
      }

      memmove(argv + i, argv + i + 2, (argc - i - 2) * sizeof(*argv));
      argc -= 2;
    }
    else if (strcmp(argv[i], "-N") == 0 || strcmp(argv[i], "--num-subdivisions") == 0) {
      if (i + 1 == argc) print_usage(argv[0], 1);

      char *end = NULL;
      opts->num_subdivisions = strtoul(argv[i + 1], &end, 10);
      if (!end || *end != '\0' || opts->chunk_size == 0) {
        fprintf(stderr, "%s: error: invalid number of subproblems `%s'\n",
                argv[0], argv[i+1]);
        print_usage(argv[0], 1);
      }

      memmove(argv + i, argv + i + 2, (argc - i - 2) * sizeof(*argv));
      argc -= 2;
    }
    else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
      print_usage(argv[0], 0);
    }
    else
      i++;
  }

  if (argc > 2 || !opts->server_name)
    print_usage(argv[0], 1);

  if (argc == 2) opts->filename = argv[1];
}

static
void print_usage(const char *program_name, int status) {
  FILE *out = status == 0 ? stdout : stderr;

  fprintf(out, "Usage: %s [OPTIONS] [INPUT_MESH]\n", program_name);
  fprintf(out, "\n");
  fprintf(out, "Distributed program to lists the hexahedral meshes bounded by the quad mesh stored "
          "in INPUT_MESH.\n");
  fprintf(out, "\n");
  fprintf(out, "  -H, --h-max N\n");
  fprintf(out, "     Specifies the maximum number of hexahedra.\n");
  fprintf(out, "  -V, --v-max N\n");
  fprintf(out, "     Specifies the maximum number of interior vertices.\n");
  fprintf(out, "  -S, --symmetries FILENAME\n");
  fprintf(out, "     Explicitly specifies the symmetries that should be accounted for from\n"
               "     a file. Hexahedral meshes that are equivalent after applying one of\n"
               "     those symmetries are only output once. By default, all symmetries of\n"
               "     the input mesh are computed automatically.\n");
  fprintf(out, "  -s, --shelable-only\n");
  fprintf(out, "     Only list shellable hexahedral meshes. This can be significantly faster\n"
               "     if not all solutions are needed.\n");
  fprintf(out, "  --non-bipartite-graph\n");
  fprintf(out, "     Disable optimizations that assume the mesh graph to be bipartite. This is needed\n"
               "     to enumerate all solutions for certain non-ball domains (e.g. the torus).");
  fprintf(out, "  -p, --precomputed FILENAME\n");
  fprintf(out, "     Loads a list of hexahedral meshes from FILENAME, alonge with their boundaries.\n"
               "     A solution is output whenever the boundary of the unmeshed region matches\n"
               "     one of the meshes in this file. Duplicate solutions may appear.\n"
               "\n"
               "     Ignored unless the `--shelable-only' flag was also provided.\n");
  fprintf(out, "  -L, --lower-bounds FILENAME\n");
  fprintf(out, "     Loads a list of quad meshes along with the minimum number of hexahedra and \n"
          "     vertices needed to mesh them. Used to speed up the search by pruning nodes\n"
          "     when these quad meshes are encountered.\n");
  fprintf(out, "  -N, --num-subdivisions N\n");
  fprintf(out, "    Number of subproblems to create and send to the server.\n");
  fprintf(out, "  -C, --chunk-size N\n");
  fprintf(out, "    Number of subproblems to request simultaneously from the server. Machines with more\n"
               "    cores need to use larger values.\n");
  fprintf(out, "  --server HOSTNAME\n");
  fprintf(out, "    Address of the server to send the subproblems to.\n");
  fprintf(out, "  -h, --help\n");
  fprintf(out, "     Displays this message.\n");
  exit(status);
}
